/**
 * 
 */
package org.swing.utility.net.client;

import java.io.IOException;
import java.io.PrintStream;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.Socket;
import java.net.SocketAddress;
import java.net.UnknownHostException;
import java.nio.BufferUnderflowException;
import java.nio.ByteBuffer;
import java.nio.channels.AlreadyConnectedException;
import java.nio.channels.ClosedChannelException;
import java.nio.channels.DatagramChannel;
import java.nio.channels.SelectableChannel;
import java.nio.channels.SocketChannel;
import java.util.ArrayList;
import java.util.List;

import org.swing.utility.net.bundle.OSCBundle;
import org.swing.utility.net.exception.OSCException;
import org.swing.utility.net.imp.OSCChannel;
import org.swing.utility.net.imp.OSCListener;
import org.swing.utility.net.message.OSCMessage;
import org.swing.utility.net.packet.OSCPacket;
import org.swing.utility.net.packet.OSCPacketCodec;
import org.swing.utility.net.util.NetUtil;

/**
 * @author lqnhu
 *
 */
public abstract class OSCReceiver implements OSCChannel, Runnable {
	private final List collListeners = new ArrayList();
	protected Thread thread = null;
	// private Map map = null;
	protected final Object generalSync = new Object(); // mutual exclusion
														// startListening /
														// stopListening
	protected final Object threadSync = new Object(); // communication with
														// receiver thread
	protected boolean isListening = false;
	private final Object bufSync = new Object(); // buffer (re)allocation
	private int bufSize = DEFAULTBUFSIZE;
	protected ByteBuffer byteBuf = null;
	protected boolean allocBuf = true;
	private int dumpMode = kDumpOff;
	private PrintStream printStream = null;
	private OSCPacketCodec c;
	private final String protocol;
	protected final InetSocketAddress localAddress;
	protected final boolean revivable;
	protected SocketAddress target = null;

	protected OSCReceiver(OSCPacketCodec c, String protocol,
			InetSocketAddress localAddress, boolean revivable) {
		this.c = c;
		this.protocol = protocol;
		this.localAddress = localAddress;
		this.revivable = revivable;
	}

	/**
	 * Creates a new instance of a revivable <code>OSCReceiver</code>, using
	 * default codec and a specific transport protocol. It picks an arbitrary
	 * free port and uses the local machine's IP. To determine the resulting
	 * port, you can use <code>getLocalAddress</code> afterwards.
	 * <P>
	 * <B>TCP</B> receivers are required to be connected to one particular
	 * target, so <code>setTarget</code> is must be called prior to
	 * <code>connect</code> or <code>startListening</code>!
	 *
	 * @param protocol
	 *            the protocol to use, currently either <code>UDP</code> or
	 *            <code>TCP</code>
	 * @return the newly created receiver
	 *
	 * @throws IOException
	 *             if a networking error occurs while creating the socket
	 * @throws IllegalArgumentException
	 *             if an illegal protocol is used
	 *
	 * @see OSCChannel#UDP
	 * @see OSCChannel#TCP
	 * @see #getLocalAddress
	 */
	public static OSCReceiver newUsing(String protocol) throws IOException {
		return newUsing(OSCPacketCodec.getDefaultCodec(), protocol);
	}

	/**
	 * Creates a new instance of a revivable <code>OSCReceiver</code>, using a
	 * specific codec and transport protocol. It picks an arbitrary free port
	 * and uses the local machine's IP. To determine the resulting port, you can
	 * use <code>getLocalAddress</code> afterwards.
	 * <P>
	 * <B>TCP</B> receivers are required to be connected to one particular
	 * target, so <code>setTarget</code> is must be called prior to
	 * <code>connect</code> or <code>startListening</code>!
	 *
	 * @param c
	 *            the codec to use
	 * @param protocol
	 *            the protocol to use, currently either <code>UDP</code> or
	 *            <code>TCP</code>
	 * @return the newly created receiver
	 *
	 * @throws IOException
	 *             if a networking error occurs while creating the socket
	 * @throws IllegalArgumentException
	 *             if an illegal protocol is used
	 *
	 * @see OSCChannel#UDP
	 * @see OSCChannel#TCP
	 * @see #getLocalAddress
	 *
	 * @since NetUtil 0.33
	 */
	public static OSCReceiver newUsing(OSCPacketCodec c, String protocol)
			throws IOException {
		return newUsing(c, protocol, 0);
	}

	/**
	 * Creates a new instance of a revivable <code>OSCReceiver</code>, using
	 * default codec and a specific transport protocol and port. It uses the
	 * local machine's IP. Note that the <code>port</code> specifies the local
	 * socket (at which the receiver listens), it does not determine the remote
	 * sockets from which messages can be received. If you want to filter out a
	 * particular remote (or target) socket, this can be done using the
	 * <code>setTarget</code> method!
	 * <P>
	 * <B>TCP</B> receivers are required to be connected to one particular
	 * target, so <code>setTarget</code> is must be called prior to
	 * <code>connect</code> or <code>startListening</code>!
	 *
	 * @param protocol
	 *            the protocol to use, currently either <code>UDP</code> or
	 *            <code>TCP</code>
	 * @param port
	 *            the port number for the OSC socket, or <code>0</code> to use
	 *            an arbitrary free port
	 * @return the newly created receiver
	 *
	 * @throws IOException
	 *             if a networking error occurs while creating the socket
	 * @throws IllegalArgumentException
	 *             if an illegal protocol is used
	 */
	public static OSCReceiver newUsing(String protocol, int port)
			throws IOException {
		return newUsing(OSCPacketCodec.getDefaultCodec(), protocol, 0);
	}

	/**
	 * Creates a new instance of a revivable <code>OSCReceiver</code>, using a
	 * specific codec and transport protocol and port. It uses the local
	 * machine's IP. Note that the <code>port</code> specifies the local socket
	 * (at which the receiver listens), it does not determine the remote sockets
	 * from which messages can be received. If you want to filter out a
	 * particular remote (or target) socket, this can be done using the
	 * <code>setTarget</code> method!
	 * <P>
	 * <B>TCP</B> receivers are required to be connected to one particular
	 * target, so <code>setTarget</code> is must be called prior to
	 * <code>connect</code> or <code>startListening</code>!
	 *
	 * @param c
	 *            the codec to use
	 * @param protocol
	 *            the protocol to use, currently either <code>UDP</code> or
	 *            <code>TCP</code>
	 * @param port
	 *            the port number for the OSC socket, or <code>0</code> to use
	 *            an arbitrary free port
	 * @return the newly created receiver
	 *
	 * @throws IOException
	 *             if a networking error occurs while creating the socket
	 * @throws IllegalArgumentException
	 *             if an illegal protocol is used
	 *
	 * @since NetUtil 0.33
	 */
	public static OSCReceiver newUsing(OSCPacketCodec c, String protocol,
			int port) throws IOException {
		return newUsing(c, protocol, 0, false);
	}

	/**
	 * Creates a new instance of a revivable <code>OSCReceiver</code>, using
	 * default codec and a specific transport protocol and port. It uses the
	 * local machine's IP or the &quot;loopback&quot; address. Note that the
	 * <code>port</code> specifies the local socket (at which the receiver
	 * listens), it does not determine the remote sockets from which messages
	 * can be received. If you want to filter out a particular remote (or
	 * target) socket, this can be done using the <code>setTarget</code> method!
	 * <P>
	 * <B>TCP</B> receivers are required to be connected to one particular
	 * target, so <code>setTarget</code> is must be called prior to
	 * <code>connect</code> or <code>startListening</code>!
	 *
	 * @param protocol
	 *            the protocol to use, currently either <code>UDP</code> or
	 *            <code>TCP</code>
	 * @param port
	 *            the port number for the OSC socket, or <code>0</code> to use
	 *            an arbitrary free port
	 * @param loopBack
	 *            if <code>true</code>, the &quot;loopback&quot; address (
	 *            <code>&quot;127.0.0.1&quot;</code>) is used which limits
	 *            communication to the local machine. If <code>false</code>, the
	 *            special IP <code>"0.0.0.0"</code> is used which means messages
	 *            from any IP as well as from the loopback are accepted
	 *
	 * @return the newly created receiver
	 *
	 * @throws IOException
	 *             if a networking error occurs while creating the socket
	 * @throws IllegalArgumentException
	 *             if an illegal protocol is used
	 */
	public static OSCReceiver newUsing(String protocol, int port,
			boolean loopBack) throws IOException {
		return newUsing(OSCPacketCodec.getDefaultCodec(), protocol, port,
				loopBack);
	}

	/**
	 * Creates a new instance of a revivable <code>OSCReceiver</code>, using a
	 * specific codec and transport protocol and port. It uses the local
	 * machine's IP or the &quot;loopback&quot; address. Note that the
	 * <code>port</code> specifies the local socket (at which the receiver
	 * listens), it does not determine the remote sockets from which messages
	 * can be received. If you want to filter out a particular remote (or
	 * target) socket, this can be done using the <code>setTarget</code> method!
	 * <P>
	 * <B>TCP</B> receivers are required to be connected to one particular
	 * target, so <code>setTarget</code> is must be called prior to
	 * <code>connect</code> or <code>startListening</code>!
	 *
	 * @param c
	 *            the codec to use
	 * @param protocol
	 *            the protocol to use, currently either <code>UDP</code> or
	 *            <code>TCP</code>
	 * @param port
	 *            the port number for the OSC socket, or <code>0</code> to use
	 *            an arbitrary free port
	 * @param loopBack
	 *            if <code>true</code>, the &quot;loopback&quot; address (
	 *            <code>&quot;127.0.0.1&quot;</code>) is used which limits
	 *            communication to the local machine. If <code>false</code>, the
	 *            special IP <code>"0.0.0.0"</code> is used which means messages
	 *            from any IP as well as from the loopback are accepted
	 *
	 * @return the newly created receiver
	 *
	 * @throws IOException
	 *             if a networking error occurs while creating the socket
	 * @throws IllegalArgumentException
	 *             if an illegal protocol is used
	 *
	 * @since NetUtil 0.33
	 */
	public static OSCReceiver newUsing(OSCPacketCodec c, String protocol,
			int port, boolean loopBack) throws IOException {
		// final InetSocketAddress localAddress = loopBack ? new
		// InetSocketAddress( "127.0.0.1", port ) :
		// new InetSocketAddress( InetAddress.getLocalHost(), port );
		final InetSocketAddress localAddress = new InetSocketAddress(
				loopBack ? "127.0.0.1" : "0.0.0.0", port);
		return newUsing(c, protocol, localAddress);
	}

	/**
	 * Creates a new instance of a revivable <code>OSCReceiver</code>, using
	 * default codec and a specific transport protocol and local socket address.
	 * Note that <code>localAdress</code> specifies the local socket (at which
	 * the receiver listens), it does not determine the remote sockets from
	 * which messages can be received. If you want to filter out a particular
	 * remote (or target) socket, this can be done using the
	 * <code>setTarget</code> method!
	 * <P>
	 * <B>TCP</B> receivers are required to be connected to one particular
	 * target, so <code>setTarget</code> is must be called prior to
	 * <code>connect</code> or <code>startListening</code>!
	 *
	 * @param protocol
	 *            the protocol to use, currently either <code>UDP</code> or
	 *            <code>TCP</code>
	 * @param localAddress
	 *            a valid address to use for the OSC socket. If the port is
	 *            <code>0</code>, an arbitrary free port is picked when the
	 *            receiver is started. (you can find out the actual port in this
	 *            case by calling <code>getLocalAddress()</code> after the
	 *            receiver was started).
	 *
	 * @return the newly created receiver
	 *
	 * @throws IOException
	 *             if a networking error occurs while creating the socket
	 * @throws IllegalArgumentException
	 *             if an illegal protocol is used
	 */
	public static OSCReceiver newUsing(String protocol,
			InetSocketAddress localAddress) throws IOException {
		return newUsing(OSCPacketCodec.getDefaultCodec(), protocol,
				localAddress);
	}

	/**
	 * Creates a new instance of a revivable <code>OSCReceiver</code>, using a
	 * specific codec and transport protocol and local socket address. Note that
	 * the <code>port</code> specifies the local socket (at which the receiver
	 * listens), it does not determine the remote sockets from which messages
	 * can be received. If you want to filter out a particular remote (or
	 * target) socket, this can be done using the <code>setTarget</code> method!
	 * <P>
	 * <B>TCP</B> receivers are required to be connected to one particular
	 * target, so <code>setTarget</code> is must be called prior to
	 * <code>connect</code> or <code>startListening</code>!
	 *
	 * @param c
	 *            the codec to use
	 * @param protocol
	 *            the protocol to use, currently either <code>UDP</code> or
	 *            <code>TCP</code>
	 * @param localAddress
	 *            a valid address to use for the OSC socket. If the port is
	 *            <code>0</code>, an arbitrary free port is picked when the
	 *            receiver is started. (you can find out the actual port in this
	 *            case by calling <code>getLocalAddress()</code> after the
	 *            receiver was started).
	 *
	 * @return the newly created receiver
	 *
	 * @throws IOException
	 *             if a networking error occurs while creating the socket
	 * @throws IllegalArgumentException
	 *             if an illegal protocol is used
	 *
	 * @since NetUtil 0.33
	 */
	public static OSCReceiver newUsing(OSCPacketCodec c, String protocol,
			InetSocketAddress localAddress) throws IOException {
		if (protocol.equals(UDP)) {
			return new UDPOSCReceiver(c, localAddress);
		} else if (protocol.equals(TCP)) {
			return new TCPOSCReceiver(c, localAddress);
		} else {
			throw new IllegalArgumentException(
					NetUtil.getResourceString("errUnknownProtocol") + protocol);
		}
	}

	/**
	 * Creates a new instance of a non-revivable <code>OSCReceiver</code>, using
	 * default codec and UDP transport on a given channel. The caller should
	 * ensure that the provided channel's socket was bound to a valid address
	 * (using <code>dch.socket().bind( SocketAddress )</code>). Note that
	 * <code>dch</code> specifies the local socket (at which the receiver
	 * listens), it does not determine the remote sockets from which messages
	 * can be received. If you want to filter out a particular remote (or
	 * target) socket, this can be done using the <code>setTarget</code> method!
	 *
	 * @param dch
	 *            the <code>DatagramChannel</code> to use as UDP socket.
	 * @return the newly created receiver
	 *
	 * @throws IOException
	 *             if a networking error occurs while configuring the socket
	 */
	public static OSCReceiver newUsing(DatagramChannel dch) throws IOException {
		return newUsing(OSCPacketCodec.getDefaultCodec(), dch);
	}

	/**
	 * Creates a new instance of a non-revivable <code>OSCReceiver</code>, using
	 * a specific codec and UDP transport on a given channel. The caller should
	 * ensure that the provided channel's socket was bound to a valid address
	 * (using <code>dch.socket().bind( SocketAddress )</code>). Note that
	 * <code>dch</code> specifies the local socket (at which the receiver
	 * listens), it does not determine the remote sockets from which messages
	 * can be received. If you want to filter out a particular remote (or
	 * target) socket, this can be done using the <code>setTarget</code> method!
	 *
	 * @param c
	 *            the codec to use
	 * @param dch
	 *            the <code>DatagramChannel</code> to use as UDP socket.
	 * @return the newly created receiver
	 *
	 * @throws IOException
	 *             if a networking error occurs while configuring the socket
	 *
	 * @since NetUtil 0.33
	 */
	public static OSCReceiver newUsing(OSCPacketCodec c, DatagramChannel dch)
			throws IOException {
		return new UDPOSCReceiver(c, dch);
	}

	/**
	 * Creates a new instance of a non-revivable <code>OSCReceiver</code>, using
	 * default codec and TCP transport on a given channel. The caller should
	 * ensure that the provided channel's socket was bound to a valid address
	 * (using <code>sch.socket().bind( SocketAddress )</code>). Furthermore, the
	 * channel must be connected (using <code>connect()</code>) before being
	 * able to receive messages. Note that <code>sch</code> specifies the local
	 * socket (at which the receiver listens), it does not determine the remote
	 * sockets from which messages can be received. The remote (or target)
	 * socket must be explicitly specified using <code>setTarget</code> before
	 * trying to connect!
	 *
	 * @param sch
	 *            the <code>SocketChannel</code> to use as TCP socket.
	 * @return the newly created receiver
	 *
	 * @throws IOException
	 *             if a networking error occurs while configuring the socket
	 */
	public static OSCReceiver newUsing(SocketChannel sch) throws IOException {
		return newUsing(OSCPacketCodec.getDefaultCodec(), sch);
	}

	/**
	 * Creates a new instance of a non-revivable <code>OSCReceiver</code>, using
	 * a specific codec and TCP transport on a given channel. The caller should
	 * ensure that the provided channel's socket was bound to a valid address
	 * (using <code>sch.socket().bind( SocketAddress )</code>). Furthermore, the
	 * channel must be connected (using <code>connect()</code>) before being
	 * able to receive messages. Note that <code>sch</code> specifies the local
	 * socket (at which the receiver listens), it does not determine the remote
	 * sockets from which messages can be received. The remote (or target)
	 * socket must be explicitly specified using <code>setTarget</code> before
	 * trying to connect!
	 *
	 * @param c
	 *            the codec to use
	 * @param sch
	 *            the <code>SocketChannel</code> to use as TCP socket.
	 * @return the newly created receiver
	 *
	 * @throws IOException
	 *             if a networking error occurs while configuring the socket
	 *
	 * @since NetUtil 0.33
	 */
	public static OSCReceiver newUsing(OSCPacketCodec c, SocketChannel sch)
			throws IOException {
		return new TCPOSCReceiver(c, sch);
	}

	public String getProtocol() {
		return protocol;
	}

	/**
	 * Queries the receiver's local socket address. You can determine the host
	 * and port from the returned address by calling <code>getHostName()</code>
	 * (or for the IP <code>getAddress().getHostAddress()</code>) and
	 * <code>getPort()</code>. This port number may be <code>0</code> if the
	 * receiver was called with an unspecified port and has not yet been
	 * started. In this case, to determine the port actually used, call this
	 * method after the receiver has been started.
	 * <p>
	 * Note that if the receiver is bound to the accept-any IP
	 * <code>"0.0.0.0"</code>, which happens for example when calling
	 * <code>newUsing( &lt;protocol&gt;, 0, false )</code>, the returned IP will
	 * be the localhost's IP, so you can patch the result directly into any
	 * <code>setTarget</code> call.
	 *
	 * @return the address of the receiver's local socket.
	 * @throws IOException
	 *             if the local host could not be resolved
	 *
	 * @see java.net.InetSocketAddress#getHostName()
	 * @see java.net.InetSocketAddress#getAddress()
	 * @see java.net.InetSocketAddress#getPort()
	 *
	 */
	public abstract InetSocketAddress getLocalAddress() throws IOException;

	public abstract void setTarget(SocketAddress target);

	public void setCodec(OSCPacketCodec c) {
		this.c = c;
	}

	public OSCPacketCodec getCodec() {
		return c;
	}

	/**
	 * Registers a listener that gets informed about incoming messages. You can
	 * call this both when listening was started and stopped.
	 *
	 * @param listener
	 *            the listener to register
	 */
	public void addOSCListener(OSCListener listener) {
		synchronized (collListeners) {
			// if( collListeners.contains( listener )) return;
			collListeners.add(listener);
		}
	}

	/**
	 * Unregisters a listener that gets informed about incoming messages
	 *
	 * @param listener
	 *            the listener to remove from the list of notified objects.
	 */
	public void removeOSCListener(OSCListener listener) {
		synchronized (collListeners) {
			collListeners.remove(listener);
		}
	}

	/**
	 * Starts to wait for incoming messages. See the class constructor
	 * description to learn how connected and unconnected channels are handled.
	 * You should never modify the the channel's setup between the constructor
	 * and calling <code>startListening</code>. This method will check the
	 * connection status of the channel, using <code>isConnected</code> and
	 * establish the connection if necessary. Therefore, calling
	 * <code>connect</code> prior to <code>startListening</code> is not
	 * necessary.
	 * <p>
	 * To find out at which port we are listening, call
	 * <code>getLocalAddress().getPort()</code>.
	 * <p>
	 * If the <code>OSCReceiver</code> is already listening, this method does
	 * nothing.
	 *
	 * @throws IOException
	 *             when an error occurs while establishing the channel
	 *             connection. In that case, no thread has been started and
	 *             hence stopListening() needn't be called
	 *
	 * @throws IllegalStateException
	 *             when trying to call this method from within the OSC receiver
	 *             thread (which would obviously cause a loop)
	 */
	public void startListening() throws IOException {
		synchronized (generalSync) {
			if (Thread.currentThread() == thread)
				throw new IllegalStateException(
						NetUtil.getResourceString("errNotInThisThread"));
			if (isListening && ((thread == null) || !thread.isAlive())) {
				isListening = false;
			}
			if (!isListening) {
				if (!isConnected())
					connect();
				isListening = true;
				thread = new Thread(this, "OSCReceiver");
				thread.setDaemon(true);
				thread.start();
			}
		}
	}

	/**
	 * Queries whether the <code>OSCReceiver</code> is listening or not.
	 */
	public boolean isListening() {
		synchronized (generalSync) {
			return isListening;
		}
	}

	/**
	 * Stops waiting for incoming messages. This method returns when the
	 * receiving thread has terminated. To prevent deadlocks, this method
	 * cancels after five seconds, calling <code>close()</code> on the datagram
	 * channel, which causes the listening thread to die because of a
	 * channel-closing exception.
	 *
	 * @throws IOException
	 *             if an error occurs while shutting down
	 *
	 * @throws IllegalStateException
	 *             when trying to call this method from within the OSC receiver
	 *             thread (which would obviously cause a loop)
	 */
	public void stopListening() throws IOException {
		synchronized (generalSync) {
			if (Thread.currentThread() == thread)
				throw new IllegalStateException(
						NetUtil.getResourceString("errNotInThisThread"));
			if (isListening) {
				isListening = false;
				if ((thread != null) && thread.isAlive()) {
					try {
						synchronized (threadSync) {
							sendGuardSignal();
							// guard.send( guardPacket );
							threadSync.wait(5000);
						}
					} catch (InterruptedException e2) {
						e2.printStackTrace();
					}
					// catch( IOException e1 ) {
					// System.err.println( "OSCReceiver.stopListening : "+e1 );
					// throw e1;
					// }
					finally {
						if ((thread != null) && thread.isAlive()) {
							try {
								System.err
										.println("OSCReceiver.stopListening : rude task killing ("
												+ this.hashCode() + ")");
								// ch.close(); // rude task killing
								closeChannel();
							} catch (IOException e3) {
								e3.printStackTrace();
								// System.err.println(
								// "OSCReceiver.stopListening 2: "+e3 );
							}
						}
						thread = null;
					}
				}
			}
		}
	}

	public void setBufferSize(int size) {
		synchronized (bufSync) {
			if (isListening)
				throw new IllegalStateException(
						NetUtil.getResourceString("errNotWhileActive"));
			if (bufSize != size) {
				bufSize = size;
				allocBuf = true;
			}
		}
	}

	public int getBufferSize() {
		synchronized (bufSync) {
			return bufSize;
		}
	}

	public void dumpOSC(int mode, PrintStream stream) {
		this.dumpMode = mode;
		this.printStream = stream == null ? System.err : stream;
	}

	public void dispose() {
		try {
			stopListening();
		} catch (IOException e1) {
			e1.printStackTrace();
		}
		try {
			// ch.close();
			closeChannel();
		} catch (IOException e1) {
			e1.printStackTrace();
		}
		collListeners.clear();
		byteBuf = null;
	}

	// /**
	// * Call this method to supply a map of custom
	// * OSCMessage subclasses used for decoding received
	// * messages.
	// *
	// * @param map a map whose keys are OSC command strings and whose
	// * values are <code>OSCMessage</code> subclasses which will
	// * be used for decoding in case the OSC command
	// * matches the map entry. For instance you might
	// * want to have a key <code>"/b_setn"</code> and a special subclass
	// * <code>OSCBufSetNMessage</code> for faster and efficient decoding
	// * of this type of OSC message.
	// *
	// * @deprecated a future version will feature special codec classes which
	// are
	// * not subclasses of <code>OSCMessage</code>! Prepare for API change!
	// */
	// public void setCustomMessageDecoders( Map map )
	// {
	// this.map = map;
	// }
	protected abstract void sendGuardSignal() throws IOException;

	public abstract void setChannel(SelectableChannel ch) throws IOException;

	protected abstract void closeChannel() throws IOException;

	protected static String debugTimeString() {
		return new java.text.SimpleDateFormat("HH:mm:ss.SSS")
				.format(new java.util.Date());
	}

	protected void flipDecodeDispatch(SocketAddress sender) throws IOException {
		final OSCPacket p;
		try {
			byteBuf.flip();
			// p = OSCPacket.decode( byteBuf, map );
			// p = OSCPacket.decode( byteBuf );
			p = c.decode(byteBuf);
			if (dumpMode != kDumpOff) {
				printStream.print("r: ");
				if ((dumpMode & kDumpText) != 0)
					OSCPacket.printTextOn(printStream, p);
				if ((dumpMode & kDumpHex) != 0) {
					byteBuf.flip();
					OSCPacket.printHexOn(printStream, byteBuf);
				}
			}
			dispatchPacket(p, sender, OSCBundle.NOW); // OSCBundles will
														// override this dummy
														// time tag
		} catch (BufferUnderflowException e1) {
			if (isListening) {
				System.err.println(new OSCException(OSCException.RECEIVE, e1
						.toString()));
			}
		}
	}

	private void dispatchPacket(OSCPacket p, SocketAddress sender, long time) {
		if (p instanceof OSCMessage) {
			dispatchMessage((OSCMessage) p, sender, time);
		} else if (p instanceof OSCBundle) {
			final OSCBundle bndl = (OSCBundle) p;
			time = bndl.getTimeTag();
			for (int i = 0; i < bndl.getPacketCount(); i++) {
				dispatchPacket(bndl.getPacket(i), sender, time);
			}
		} else {
			assert false : p.getClass().getName();
		}
	}

	private void dispatchMessage(OSCMessage msg, SocketAddress sender, long time) {
		OSCListener listener;
		synchronized (collListeners) {
			for (int i = 0; i < collListeners.size(); i++) {
				listener = (OSCListener) collListeners.get(i);
				// try {
				listener.messageReceived(msg, sender, time);
				// }
				// catch( java.lang.RuntimeException e1 ) {
				// e1.printStackTrace();
				// }
			}
		}
	}

	protected void checkBuffer() {
		synchronized (bufSync) {
			if (allocBuf) {
				byteBuf = ByteBuffer.allocateDirect(bufSize);
				allocBuf = false;
			}
		}
	}

	protected InetSocketAddress getLocalAddress(InetAddress addr, int port)
			throws UnknownHostException {
		return new InetSocketAddress(
				addr.getHostName().equals("0.0.0.0") ? InetAddress.getLocalHost()
						: addr, port);
	}

	/**
	 * Establishes connection for transports requiring connectivity (e.g. TCP).
	 * For transports that do not require connectivity (e.g. UDP), this ensures
	 * the communication channel is created and bound.
	 * <P>
	 * Having a connected channel without actually listening to incoming
	 * messages is usually not making sense. You can call
	 * <code>startListening</code> without explicit prior call to
	 * <code>connect</code>, because <code>startListening</code> will establish
	 * the connection if necessary.
	 * <P>
	 * When a <B>UDP</B> transmitter is created without an explicit
	 * <code>DatagramChannel</code> &ndash; say by calling
	 * <code>OSCReceiver.newUsing( &quot;udp&quot; )</code>, calling
	 * <code>connect()</code> will actually create and bind a
	 * <code>DatagramChannel</code>. For a <B>UDP</B> receiver which was created
	 * with an explicit <code>DatagramChannel</code>. However, for <B>TCP</B>
	 * receivers, this may throw an <code>IOException</code> if the receiver was
	 * already connected, therefore be sure to check <code>isConnected()</code>
	 * before.
	 *
	 * @throws IOException
	 *             if a networking error occurs. Possible reasons: - the
	 *             underlying network channel had been closed by the server. -
	 *             the transport is TCP and the server is not available.
	 *
	 * @see #isConnected()
	 * @see #startListening()
	 * @throws IOException
	 */
	public abstract void connect() throws IOException;

	/**
	 * Queries the connection state of the receiver.
	 *
	 * @return <code>true</code> if the receiver is connected,
	 *         <code>false</code> otherwise. For transports that do not use
	 *         connectivity (e.g. UDP) this returns <code>false</code>, if the
	 *         underlying <code>DatagramChannel</code> has not yet been created.
	 *
	 * @see #connect()
	 */
	public abstract boolean isConnected();

	// --------------------- internal classes ---------------------
	private static class UDPOSCReceiver extends OSCReceiver {
		private DatagramChannel dch;

		protected UDPOSCReceiver(OSCPacketCodec c,
				InetSocketAddress localAddress) throws IOException {
			super(c, UDP, localAddress, true);
		}

		protected UDPOSCReceiver(OSCPacketCodec c, DatagramChannel dch)
				throws IOException {
			super(c, UDP, new InetSocketAddress(dch.socket().getLocalAddress(),
					dch.socket().getLocalPort()), false);
			this.dch = dch;
		}

		public void setChannel(SelectableChannel ch) throws IOException {
			synchronized (generalSync) {
				if (isListening)
					throw new IllegalStateException(
							NetUtil.getResourceString("errNotWhileActive"));
				dch = (DatagramChannel) ch;
				if (!dch.isBlocking()) {
					dch.configureBlocking(true);
				}
				if (dch.isConnected())
					throw new IllegalStateException(
							NetUtil.getResourceString("errChannelConnected"));
			}
		}

		public InetSocketAddress getLocalAddress() throws IOException {
			synchronized (generalSync) {
				if (dch != null) {
					final DatagramSocket ds = dch.socket();
					return getLocalAddress(ds.getLocalAddress(),
							ds.getLocalPort());
				} else {
					return getLocalAddress(localAddress.getAddress(),
							localAddress.getPort());
				}
			}
		}

		public void setTarget(SocketAddress target) {
			this.target = target;
		}

		public void connect() throws IOException {
			synchronized (generalSync) {
				if (isListening)
					throw new IllegalStateException(
							NetUtil.getResourceString("errNotWhileActive"));
				if ((dch != null) && !dch.isOpen()) {
					if (!revivable)
						throw new IOException(
								NetUtil.getResourceString("errCannotRevive"));
					dch = null;
				}
				if (dch == null) {
					final DatagramChannel newCh = DatagramChannel.open();
					newCh.socket().bind(localAddress);
					// dch = newCh;
					setChannel(newCh);
				}
			}
		}

		public boolean isConnected() {
			synchronized (generalSync) {
				return ((dch != null) && dch.isOpen());
			}
		}

		protected void closeChannel() throws IOException {
			if (dch != null) {
				try {
					dch.close();
				} finally {
					dch = null;
				}
			}
		}

		/**
		 * This is the body of the listening thread
		 */
		public void run() {
			SocketAddress sender;
			checkBuffer();
			try {
				listen: while (isListening) {
					try {
						byteBuf.clear();
						sender = dch.receive(byteBuf);
						if (!isListening)
							break listen;
						if (sender == null)
							continue listen;
						if ((target != null) && !target.equals(sender))
							continue listen;
						flipDecodeDispatch(sender);
					} catch (ClosedChannelException e1) { // bye bye, we have to
															// quit
						if (isListening) {
							// System.err.println( e1 );
							System.err.println("OSCReceiver.run : "
									+ e1.getClass().getName() + " : "
									+ e1.getLocalizedMessage());
						}
						return;
					} catch (IOException e1) {
						if (isListening) {
							System.err.println("OSCReceiver.run : "
									+ e1.getClass().getName() + " : "
									+ e1.getLocalizedMessage());
							// System.err.println( new OSCException(
							// OSCException.RECEIVE, e1.toString() ));
						}
					}
				} // while( isListening )
			} finally {
				synchronized (threadSync) {
					thread = null;
					threadSync.notifyAll(); // stopListening() might be waiting
				}
			}
		}

		protected void sendGuardSignal() throws IOException {
			final DatagramSocket guard;
			final DatagramPacket guardPacket;
			guard = new DatagramSocket();
			guardPacket = new DatagramPacket(new byte[0], 0);
			guardPacket.setSocketAddress(getLocalAddress());
			guard.send(guardPacket);
			guard.close();
		}
	}

	private static class TCPOSCReceiver extends OSCReceiver {
		private SocketChannel sch = null;

		protected TCPOSCReceiver(OSCPacketCodec c,
				InetSocketAddress localAddress) {
			super(c, TCP, localAddress, true);
		}

		protected TCPOSCReceiver(OSCPacketCodec c, SocketChannel sch) {
			super(c, TCP, new InetSocketAddress(sch.socket().getLocalAddress(),
					sch.socket().getLocalPort()), false);
			this.sch = sch;
		}

		public void setChannel(SelectableChannel ch) throws IOException {
			synchronized (generalSync) {
				if (isListening)
					throw new IllegalStateException(
							NetUtil.getResourceString("errNotWhileActive"));
				sch = (SocketChannel) ch;
				if (!sch.isBlocking()) {
					sch.configureBlocking(true);
				}
			}
		}

		public InetSocketAddress getLocalAddress() throws IOException {
			synchronized (generalSync) {
				if (sch != null) {
					final Socket s = sch.socket();
					return getLocalAddress(s.getLocalAddress(),
							s.getLocalPort());
				} else {
					return getLocalAddress(localAddress.getAddress(),
							localAddress.getPort());
				}
			}
		}

		public void setTarget(SocketAddress target) {
			synchronized (generalSync) {
				if (isConnected())
					throw new AlreadyConnectedException();
				this.target = target;
			}
		}

		public void connect() throws IOException {
			synchronized (generalSync) {
				if (isListening)
					throw new IllegalStateException(
							NetUtil.getResourceString("errNotWhileActive"));
				if ((sch != null) && !sch.isOpen()) {
					if (!revivable)
						throw new IOException(
								NetUtil.getResourceString("errCannotRevive"));
					sch = null;
				}
				if (sch == null) {
					final SocketChannel newCh = SocketChannel.open();
					newCh.socket().bind(localAddress);
					sch = newCh;
				}
				if (!sch.isConnected()) {
					sch.connect(target);
				}
			}
		}

		public boolean isConnected() {
			synchronized (generalSync) {
				return ((sch != null) && sch.isConnected());
			}
		}

		protected void closeChannel() throws IOException {
			if (sch != null) {
				try {
					// System.err.println( "TCPOSCReceiver.closeChannel()" );
					sch.close();
					// System.err.println( "...ok" );
				} finally {
					sch = null;
				}
			}
		}

		public void run() {
			final SocketAddress sender = sch.socket().getRemoteSocketAddress();
			int len, packetSize;
			checkBuffer();
			try {
				listen: while (isListening) {
					try {
						byteBuf.rewind().limit(4); // in TCP mode, first four
													// bytes are packet size in
													// bytes
						do {
							len = sch.read(byteBuf);
							if (len == -1)
								break listen;
						} while (byteBuf.hasRemaining());
						byteBuf.rewind();
						packetSize = byteBuf.getInt();
						byteBuf.rewind().limit(packetSize);
						while (byteBuf.hasRemaining()) {
							len = sch.read(byteBuf);
							if (len == -1)
								break listen;
						}
						flipDecodeDispatch(sender);
						// flipDecodeDispatch( target );
					} catch (IllegalArgumentException e1) { // thrown on illegal
															// byteBuf.limit()
															// calls
						if (isListening) {
							// System.err.println( new OSCException(
							// OSCException.RECEIVE, e1.toString() ));
							final OSCException e2 = new OSCException(
									OSCException.RECEIVE, e1.toString());
							System.err.println("OSCReceiver.run : "
									+ e2.getClass().getName() + " : "
									+ e2.getLocalizedMessage());
						}
					} catch (ClosedChannelException e1) { // bye bye, we have to
															// quit
						if (isListening) {
							System.err.println("OSCReceiver.run : "
									+ e1.getClass().getName() + " : "
									+ e1.getLocalizedMessage());
						}
						return;
					} catch (IOException e1) {
						if (isListening) {
							System.err.println("OSCReceiver.run : "
									+ e1.getClass().getName() + " : "
									+ e1.getLocalizedMessage());
							// System.err.println( new OSCException(
							// OSCException.RECEIVE, e1.toString() ));
						}
					}
				}
			} finally {
				synchronized (threadSync) {
					thread = null;
					threadSync.notifyAll(); // stopListening() might be waiting
				}
			}
		}

		/**
		 * @warning this calls socket().shutdownInput() to unblock the listening
		 *          thread. unfortunately this cannot be undone, so it's not
		 *          possible to revive the receiver in TCP mode ;-( have to
		 *          check for alternative ways
		 */
		protected void sendGuardSignal() throws IOException {
			sch.socket().shutdownInput();
		}
	}
}
